Skip to content

Fix RequestResponseIO parseAndThrow to preserve retryable exception types - #37342

Open
PDGGK wants to merge 2 commits into
apache:masterfrom
PDGGK:java-fix-requestresponseio
Open

Fix RequestResponseIO parseAndThrow to preserve retryable exception types#37342
PDGGK wants to merge 2 commits into
apache:masterfrom
PDGGK:java-fix-requestresponseio

Conversation

@PDGGK

@PDGGK PDGGK commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #37176RequestResponseIO loses retryable exception types in parseAndThrow.

Call.parseAndThrow unwrapped the ExecutionException from Future.get() but only special-cased
UserCodeQuotaException. Every other UserCodeExecutionException subclass — notably
UserCodeTimeoutException and UserCodeRemoteSystemException — was re-wrapped in a plain
UserCodeExecutionException, so Call's repeater saw shouldRepeat() == false and gave up on failures
that were meant to be retried.

Fix

Future.get() wraps a user exception in exactly one ExecutionException, so a single-level
e.getCause() check is sufficient:

Throwable cause = e.getCause();
if (cause instanceof UserCodeExecutionException) {
  throw (UserCodeExecutionException) cause;
}
throw new UserCodeExecutionException(cause == null ? e : cause);

Rethrowing the cause as-is preserves the concrete subclass rather than enumerating known ones, so it also
covers any UserCodeExecutionException subclass added later. The null-cause path still produces a
UserCodeExecutionException wrapping the original ExecutionException, as before.

An earlier revision of this PR walked the whole causal chain with Guava's Throwables.getCausalChain().
That was dropped — the deep-nesting cases it handled cannot occur on this code path — and the tests that
only exercised the traversal were removed with it.

Changes

  • sdks/java/io/rrio/.../Call.javaparseAndThrow rethrows the cause when it already is a
    UserCodeExecutionException.
  • sdks/java/io/rrio/.../CallTest.java — two new tests plus one retargeted:
    • givenCallerThrowsRemoteSystemException_thenPreservesExceptionType — asserts the failure carries
      UserCodeRemoteSystemException and not a bare UserCodeExecutionException.
    • givenCallerThrowsNonUserCodeException_emitsWrappedUserCodeExecutionException — a plain
      RuntimeException is still wrapped, so the fallback path is covered.
    • givenCallerThrowsTimeoutException_emitsFailurePCollection → renamed to
      ..._thenPreservesExceptionType and tightened: the bare-UserCodeExecutionException count now has to
      be 0, which is what actually fails without the fix.

Both *_thenPreservesExceptionType tests are regression tests: they assert the bare
UserCodeExecutionException count is 0, which only holds with the Call.java change.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @PDGGK, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical issue where retryable exceptions were being incorrectly wrapped by a generic UserCodeExecutionException, thereby breaking the retry mechanism. The core change involves enhancing the exception parsing logic to traverse the full causal chain, ensuring that specific retryable exception types are preserved and re-thrown. This significantly improves the reliability of retry logic within the RequestResponseIO component.

Highlights

  • Exception Preservation: The parseAndThrow method in Call.java has been refactored to correctly identify and re-throw specific retryable exceptions (UserCodeQuotaException, UserCodeTimeoutException, UserCodeRemoteSystemException) even when they are deeply nested within other exceptions, preventing them from being generically wrapped by UserCodeExecutionException.
  • Causal Chain Traversal: The fix utilizes Guava's Throwables.getCausalChain() to thoroughly inspect the entire exception causal chain, ensuring that the original retryable exception type is not lost due to intermediate wrapping.
  • Circular Reference Handling: The updated logic now gracefully handles circular references within an exception's causal chain, preventing infinite loops and throwing a UserCodeExecutionException with a descriptive message in such cases.
  • Comprehensive Testing: Ten new unit tests have been added to CallTest.java to cover various scenarios, including direct, nested, and triple-nested retryable exceptions, generic exceptions wrapping retryable ones, non-UserCode exceptions, and circular exception chains, ensuring the robustness of the fix.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions

Copy link
Copy Markdown
Contributor

Assigning reviewers:

R: @kennknowles for label java.

Note: If you would like to opt out of this review, comment assign to next reviewer.

Available commands:

  • stop reviewer notifications - opt out of the automated review tooling
  • remind me after tests pass - tag the comment author after tests pass
  • waiting on author - shift the attention set back to the author (any comment or push by the author will return the attention set to the reviewers)

The PR bot will only process comments in the main thread (not review comments).

@github-actions

Copy link
Copy Markdown
Contributor

Reminder, please take a look at this pr: @kennknowles

@damccorm

Copy link
Copy Markdown
Contributor

@kennknowles could you please take a look?

@github-actions

github-actions Bot commented Feb 3, 2026

Copy link
Copy Markdown
Contributor

Reminder, please take a look at this pr: @kennknowles

@kennknowles
kennknowles self-requested a review February 4, 2026 17:20
@github-actions

Copy link
Copy Markdown
Contributor

Assigning new set of reviewers because Pr has gone too long without review. If you would like to opt out of this review, comment assign to next reviewer:

R: @Abacn for label java.

Available commands:

  • stop reviewer notifications - opt out of the automated review tooling
  • remind me after tests pass - tag the comment author after tests pass
  • waiting on author - shift the attention set back to the author (any comment or push by the author will return the attention set to the reviewers)

@github-actions

Copy link
Copy Markdown
Contributor

Reminder, please take a look at this pr: @Abacn

@kennknowles

Copy link
Copy Markdown
Member

I don't really remember the design well enough to quickly review just the diff. I would need to read the whole IO to understand the intent. Maybe @Abacn knows better than me. I have a couple questions / comments

  • Who is doing the retrying? Why are they not retrying the UserCodeException? Should they be? if it is inside the RRIO then how did it get wrapped into UserCodeException? If it is the runner, then UserCodeException is usually the correct way to pass the info to the runner. I just don't know the design here.
  • It seems very odd (like, something I've never seen in any code) to traverse the whole causal chain. I feel there must be a more straightforward thing we should do.

@PDGGK

PDGGK commented Feb 22, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the questions @kennknowles!

Who is doing the retrying?
Retrying happens inside RRIO, in Repeater.apply() — not by the runner. Repeater catches UserCodeExecutionException and calls e.shouldRepeat() to decide whether to retry. The three subclasses (UserCodeQuotaException, UserCodeTimeoutException, UserCodeRemoteSystemException) override shouldRepeat() to return true, while the base class returns false.

The bug is that parseAndThrow only preserved UserCodeQuotaException when unwrapping ExecutionException from Future.get(). The other two retryable types were being re-wrapped as base UserCodeExecutionException, which made shouldRepeat() return false — silently skipping retries for timeout and remote system errors.

Traversing the full causal chain
You're right — that's unnecessarily complex. Future.get() adds exactly one ExecutionException wrapper, so inspecting e.getCause() is sufficient. I'll simplify parseAndThrow to: if the cause is already a UserCodeExecutionException (or subclass), rethrow it directly; otherwise wrap it once. This is simpler and automatically handles any future subclasses.

Pushing the simplified version now.

@github-actions

github-actions Bot commented Mar 1, 2026

Copy link
Copy Markdown
Contributor

Reminder, please take a look at this pr: @Abacn @kennknowles

@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

Assigning new set of reviewers because Pr has gone too long without review. If you would like to opt out of this review, comment assign to next reviewer:

R: @ahmedabu98 for label java.

Available commands:

  • stop reviewer notifications - opt out of the automated review tooling
  • remind me after tests pass - tag the comment author after tests pass
  • waiting on author - shift the attention set back to the author (any comment or push by the author will return the attention set to the reviewers)

@github-actions

Copy link
Copy Markdown
Contributor

Reminder, please take a look at this pr: @ahmedabu98 @kennknowles

@github-actions

Copy link
Copy Markdown
Contributor

Assigning new set of reviewers because Pr has gone too long without review. If you would like to opt out of this review, comment assign to next reviewer:

R: @chamikaramj for label java.

Available commands:

  • stop reviewer notifications - opt out of the automated review tooling
  • remind me after tests pass - tag the comment author after tests pass
  • waiting on author - shift the attention set back to the author (any comment or push by the author will return the attention set to the reviewers)

@github-actions

Copy link
Copy Markdown
Contributor

Reminder, please take a look at this pr: @ahmedabu98 @kennknowles

@github-actions

Copy link
Copy Markdown
Contributor

Assigning new set of reviewers because Pr has gone too long without review. If you would like to opt out of this review, comment assign to next reviewer:

R: @chamikaramj for label java.

Available commands:

  • stop reviewer notifications - opt out of the automated review tooling
  • remind me after tests pass - tag the comment author after tests pass
  • waiting on author - shift the attention set back to the author (any comment or push by the author will return the attention set to the reviewers)

@github-actions

Copy link
Copy Markdown
Contributor

Reminder, please take a look at this pr: @chamikaramj @kennknowles

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Assigning new set of reviewers because Pr has gone too long without review. If you would like to opt out of this review, comment assign to next reviewer:

R: @ahmedabu98 for label java.

Available commands:

  • stop reviewer notifications - opt out of the automated review tooling
  • remind me after tests pass - tag the comment author after tests pass
  • waiting on author - shift the attention set back to the author (any comment or push by the author will return the attention set to the reviewers)

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Reminder, please take a look at this pr: @ahmedabu98 @kennknowles

@github-actions

Copy link
Copy Markdown
Contributor

Assigning new set of reviewers because Pr has gone too long without review. If you would like to opt out of this review, comment assign to next reviewer:

R: @Abacn for label java.

Available commands:

  • stop reviewer notifications - opt out of the automated review tooling
  • remind me after tests pass - tag the comment author after tests pass
  • waiting on author - shift the attention set back to the author (any comment or push by the author will return the attention set to the reviewers)

@damccorm

Copy link
Copy Markdown
Contributor

@kennknowles what are next steps here?

@github-actions

Copy link
Copy Markdown
Contributor

Reminder, please take a look at this pr: @Abacn @kennknowles

@github-actions

Copy link
Copy Markdown
Contributor

Assigning new set of reviewers because Pr has gone too long without review. If you would like to opt out of this review, comment assign to next reviewer:

R: @chamikaramj for label java.

Available commands:

  • stop reviewer notifications - opt out of the automated review tooling
  • remind me after tests pass - tag the comment author after tests pass
  • waiting on author - shift the attention set back to the author (any comment or push by the author will return the attention set to the reviewers)

@PDGGK

PDGGK commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

Friendly ping — this one's been waiting on review since February and CI is green / the branch is mergeable.

The questions @kennknowles raised earlier are resolved: per the feedback I simplified parseAndThrow to inspect e.getCause() once (Future.get() adds exactly one ExecutionException wrapper) — if the cause is a UserCodeExecutionException or subclass it's rethrown as-is so shouldRepeat() still sees the retryable type, otherwise it's wrapped once. That's in commit 5ff10ae and the tests were updated to match.

@chamikaramj @Abacn @kennknowles could one of you take a look when you have a chance? Happy to make any further changes.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Reminder, please take a look at this pr: @chamikaramj @kennknowles

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Assigning new set of reviewers because Pr has gone too long without review. If you would like to opt out of this review, comment assign to next reviewer:

R: @Abacn for label java.

Available commands:

  • stop reviewer notifications - opt out of the automated review tooling
  • remind me after tests pass - tag the comment author after tests pass
  • waiting on author - shift the attention set back to the author (any comment or push by the author will return the attention set to the reviewers)

@bvolpato bvolpato left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I traced the full path from CallerWithTimeout through Future.get() / parseAndThrow into Repeater. Rethrowing an existing UserCodeExecutionException is correct here: Future.get() adds one ExecutionException wrapper, and preserving the subtype keeps shouldRepeat() intact for timeout, quota, remote-system, and future subclasses. Non-RRIO exceptions remain wrapped once.

I also tested a clean synthetic merge with current master (RRIO files changed since this PR's last CI run): full :sdks:java:io:rrio:test, Spotless, and Checkstyle on Java 17, plus CallTest and RequestResponseIOTest on Java 21. All green.

Non-blocking: PR description still describes causal-chain traversal and 10 new tests, but current patch uses one-level unwrapping and a smaller test set. Worth updating before merge.

@github-actions

Copy link
Copy Markdown
Contributor

Reminder, please take a look at this pr: @Abacn @kennknowles

@github-actions

Copy link
Copy Markdown
Contributor

Assigning new set of reviewers because Pr has gone too long without review. If you would like to opt out of this review, comment assign to next reviewer:

R: @ahmedabu98 for label java.

Available commands:

  • stop reviewer notifications - opt out of the automated review tooling
  • remind me after tests pass - tag the comment author after tests pass
  • waiting on author - shift the attention set back to the author (any comment or push by the author will return the attention set to the reviewers)

PDGGK added 2 commits July 26, 2026 02:36
…ypes

Problem:
The parseAndThrow method in Call.java was wrapping retryable exceptions
(UserCodeTimeoutException, UserCodeRemoteSystemException) in a generic
UserCodeExecutionException, which breaks the retry logic that depends on
exception.shouldRepeat() returning true.

Solution:
- Scan the full causal chain using Guava's Throwables.getCausalChain()
- Preserve all specific retryable exception types (Quota/Timeout/RemoteSystem)
- Prefer specific types over generic UserCodeExecutionException when both exist
  in the chain to prevent masking of retryable exceptions
- Handle circular causal chains gracefully by catching IllegalArgumentException

Testing:
- Added 10 new unit tests covering:
  * Direct retryable exceptions (Timeout, RemoteSystem)
  * Nested exceptions (UncheckedExecutionException wrapping)
  * Generic UserCodeExecutionException masking specific types
  * Triple-nested exceptions
  * Circular reference in causal chain
  * Non-UserCode exceptions (RuntimeException)
- All existing tests pass
- Full rrio test suite passes (90 tasks)

Fixes apache#37176
…al chain

Future.get() wraps with exactly one ExecutionException, so inspecting
e.getCause() is sufficient. If the cause is a UserCodeExecutionException
(or subclass), rethrow it directly - this preserves all retryable types
and is automatically future-proof for new subclasses.

Remove deep-nesting tests that tested the now-removed causal chain
traversal, as those scenarios cannot occur in this code path.
@PDGGK
PDGGK force-pushed the java-fix-requestresponseio branch from 5ff10ae to 819af20 Compare July 25, 2026 16:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: RequestResponseIO: Call wraps retryable exceptions in UserCodeExecutionException, preventing retry/backoff

4 participants